<!DOCTYPE HTML>
<!--
	Dimension by HTML5 UP
	html5up.net | @ajlkn
	Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
-->
<html>
 <head>
  <title>
   Dimension by HTML5 UP
  </title>
  <!-- <meta charset="utf-8" /> -->
  <!-- <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /> -->
  <meta charset="utf-8"/>
  <meta content="width=device-width,initial-scale=1.0" name="viewport"/>
  <link href="../../assets/css/article.css" rel="stylesheet"/>
  <link href="https://cdn.bootcss.com/highlight.js/9.15.8/styles/github.min.css" rel="stylesheet"/>
  <noscript>
   <link href="../../assets/css/noscript.css" rel="stylesheet"/>
  </noscript>
 </head>
 <body>
  <div id="app">
  </div>
  <!-- built files will be auto injected -->
 </body>
 <body class="is-preload">
  <!-- Wrapper -->
  <div id="wrapper">
   <!-- Main -->
   <div id="main">
    <article id="article">
     <h1 id="spring-cloud-gateway">
      Spring Cloud Gateway(五) 限流
     </h1>
     <hr/>
     <h2 id="_1">
      简介
     </h2>
     <p>
      简单查看限流的处理流程，看限流filter在filters中的顺序
     </p>
     <h2 id="_2">
      分析
     </h2>
     <p>
      这次配置使用Redis的令牌桶，主函数代码如下：
     </p>
     <div class="codehilite">
      <pre><span></span><code><span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</span> <span class="p">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span> <span class="n">args</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">SpringApplication</span><span class="p">.</span><span class="na">run</span><span class="p">(</span><span class="n">Application</span><span class="p">.</span><span class="na">class</span><span class="p">,</span> <span class="n">args</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="n">RouteLocator</span> <span class="nf">myRoutes</span><span class="p">(</span><span class="n">RouteLocatorBuilder</span> <span class="n">builder</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">builder</span><span class="p">.</span><span class="na">routes</span><span class="p">()</span>
                <span class="p">.</span><span class="na">route</span><span class="p">(</span><span class="n">p</span> <span class="o">-&gt;</span> <span class="n">p</span>
                        <span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"/"</span><span class="p">)</span>
                        <span class="p">.</span><span class="na">filters</span><span class="p">(</span><span class="n">f</span> <span class="o">-&gt;</span> <span class="n">f</span><span class="p">.</span><span class="na">addRequestParameter</span><span class="p">(</span><span class="s">"test"</span><span class="p">,</span> <span class="s">"test"</span><span class="p">)</span>
                                <span class="p">.</span><span class="na">addResponseHeader</span><span class="p">(</span><span class="s">"return"</span><span class="p">,</span> <span class="s">"return"</span><span class="p">)</span>
                                <span class="p">.</span><span class="na">retry</span><span class="p">(</span><span class="n">retryConsumer</span><span class="p">)</span>
                                <span class="p">.</span><span class="na">requestRateLimiter</span><span class="p">(</span><span class="n">l</span> <span class="o">-&gt;</span> <span class="n">l</span><span class="p">.</span><span class="na">setRateLimiter</span><span class="p">(</span><span class="n">redisRateLimiter</span><span class="p">()))</span>
                        <span class="p">)</span>
                        <span class="p">.</span><span class="na">uri</span><span class="p">(</span><span class="s">"http://localhost:8082/"</span><span class="p">))</span>
                <span class="p">.</span><span class="na">build</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="cm">/**</span>
<span class="cm">     * 设置retry配置</span>
<span class="cm">     */</span>
    <span class="kd">private</span> <span class="n">Consumer</span><span class="o">&lt;</span><span class="n">RetryGatewayFilterFactory</span><span class="p">.</span><span class="na">RetryConfig</span><span class="o">&gt;</span> <span class="n">retryConsumer</span> <span class="o">=</span> <span class="n">retryConfig</span> <span class="o">-&gt;</span> <span class="p">{</span>
        <span class="n">retryConfig</span><span class="p">.</span><span class="na">setMethods</span><span class="p">(</span><span class="n">HttpMethod</span><span class="p">.</span><span class="na">values</span><span class="p">())</span>
                <span class="p">.</span><span class="na">setStatuses</span><span class="p">(</span><span class="n">HttpStatus</span><span class="p">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="p">,</span> <span class="n">HttpStatus</span><span class="p">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="p">)</span>
                <span class="p">.</span><span class="na">setRetries</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
    <span class="p">};</span>

    <span class="cm">/**</span>
<span class="cm">     * 根据 HostAddress 进行限流</span>
<span class="cm">     *</span>
<span class="cm">     * @return KeyResolver</span>
<span class="cm">     */</span>
    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="n">KeyResolver</span> <span class="nf">hostKeyResolver</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">exchange</span> <span class="o">-&gt;</span> <span class="n">Mono</span><span class="p">.</span><span class="na">just</span><span class="p">(</span><span class="n">Objects</span><span class="p">.</span><span class="na">requireNonNull</span><span class="p">(</span><span class="n">exchange</span><span class="p">.</span><span class="na">getRequest</span><span class="p">().</span><span class="na">getRemoteAddress</span><span class="p">()).</span><span class="na">getHostString</span><span class="p">());</span>
    <span class="p">}</span>

    <span class="cm">/**</span>
<span class="cm">     * Redis 令牌桶 限流</span>
<span class="cm">     *</span>
<span class="cm">     * @return RedisRateLimiter</span>
<span class="cm">     */</span>
    <span class="nd">@Bean</span>
    <span class="n">RedisRateLimiter</span> <span class="nf">redisRateLimiter</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="n">RedisRateLimiter</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>

<span class="p">}</span>
</code></pre>
     </div>
     <h3 id="_3">
      限流实现分析
     </h3>
     <p>
      在不加断点的情况下，查看输出日志，初步定位限流相关逻辑类
     </p>
     <p>
      通过查找，找到下面两个类
     </p>
     <ul>
      <li>
       RequestRateLimiterGatewayFilterFactory：filter实现类
      </li>
      <li>
       RedisRateLimiter ：具体令牌桶实现类
      </li>
     </ul>
     <p>
      通过debug，找到其相应filter次序，位于 RouteToRequestUrlFilter 之前。其核心如下：调用 RedisRateLimiter 的判断方法，得到结果后判断其状态，处于限流状态则直接返回，不触发下面的filter；反之，调用触发调用后面的filter
     </p>
     <div class="codehilite">
      <pre><span></span><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">RequestRateLimiterGatewayFilterFactory</span> <span class="kd">extends</span> 
    <span class="n">AbstractGatewayFilterFactory</span><span class="o">&lt;</span><span class="n">RequestRateLimiterGatewayFilterFactory</span><span class="p">.</span><span class="na">Config</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nd">@SuppressWarnings</span><span class="p">(</span><span class="s">"unchecked"</span><span class="p">)</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="n">GatewayFilter</span> <span class="nf">apply</span><span class="p">(</span><span class="n">Config</span> <span class="n">config</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">KeyResolver</span> <span class="n">resolver</span> <span class="o">=</span> <span class="n">getOrDefault</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="na">keyResolver</span><span class="p">,</span> <span class="n">defaultKeyResolver</span><span class="p">);</span>
        <span class="n">RateLimiter</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">limiter</span> <span class="o">=</span> <span class="n">getOrDefault</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="na">rateLimiter</span><span class="p">,</span>
                <span class="n">defaultRateLimiter</span><span class="p">);</span>
        <span class="kt">boolean</span> <span class="n">denyEmpty</span> <span class="o">=</span> <span class="n">getOrDefault</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="na">denyEmptyKey</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="na">denyEmptyKey</span><span class="p">);</span>
        <span class="n">HttpStatusHolder</span> <span class="n">emptyKeyStatus</span> <span class="o">=</span> <span class="n">HttpStatusHolder</span>
                <span class="p">.</span><span class="na">parse</span><span class="p">(</span><span class="n">getOrDefault</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="na">emptyKeyStatus</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="na">emptyKeyStatusCode</span><span class="p">));</span>

        <span class="k">return</span> <span class="p">(</span><span class="n">exchange</span><span class="p">,</span> <span class="n">chain</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">resolver</span><span class="p">.</span><span class="na">resolve</span><span class="p">(</span><span class="n">exchange</span><span class="p">).</span><span class="na">defaultIfEmpty</span><span class="p">(</span><span class="n">EMPTY_KEY</span><span class="p">)</span>
                <span class="p">.</span><span class="na">flatMap</span><span class="p">(</span><span class="n">key</span> <span class="o">-&gt;</span> <span class="p">{</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">EMPTY_KEY</span><span class="p">.</span><span class="na">equals</span><span class="p">(</span><span class="n">key</span><span class="p">))</span> <span class="p">{</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">denyEmpty</span><span class="p">)</span> <span class="p">{</span>
                            <span class="n">setResponseStatus</span><span class="p">(</span><span class="n">exchange</span><span class="p">,</span> <span class="n">emptyKeyStatus</span><span class="p">);</span>
                            <span class="k">return</span> <span class="n">exchange</span><span class="p">.</span><span class="na">getResponse</span><span class="p">().</span><span class="na">setComplete</span><span class="p">();</span>
                        <span class="p">}</span>
                        <span class="k">return</span> <span class="n">chain</span><span class="p">.</span><span class="na">filter</span><span class="p">(</span><span class="n">exchange</span><span class="p">);</span>
                    <span class="p">}</span>
                    <span class="n">String</span> <span class="n">routeId</span> <span class="o">=</span> <span class="n">config</span><span class="p">.</span><span class="na">getRouteId</span><span class="p">();</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">routeId</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">Route</span> <span class="n">route</span> <span class="o">=</span> <span class="n">exchange</span>
                                <span class="p">.</span><span class="na">getAttribute</span><span class="p">(</span><span class="n">ServerWebExchangeUtils</span><span class="p">.</span><span class="na">GATEWAY_ROUTE_ATTR</span><span class="p">);</span>
                        <span class="n">routeId</span> <span class="o">=</span> <span class="n">route</span><span class="p">.</span><span class="na">getId</span><span class="p">();</span>
                    <span class="p">}</span>
                    <span class="k">return</span> <span class="n">limiter</span><span class="p">.</span><span class="na">isAllowed</span><span class="p">(</span><span class="n">routeId</span><span class="p">,</span> <span class="n">key</span><span class="p">).</span><span class="na">flatMap</span><span class="p">(</span><span class="n">response</span> <span class="o">-&gt;</span> <span class="p">{</span>

                        <span class="k">for</span> <span class="p">(</span><span class="n">Map</span><span class="p">.</span><span class="na">Entry</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span> <span class="n">String</span><span class="o">&gt;</span> <span class="n">header</span> <span class="p">:</span> <span class="n">response</span><span class="p">.</span><span class="na">getHeaders</span><span class="p">()</span>
                                <span class="p">.</span><span class="na">entrySet</span><span class="p">())</span> <span class="p">{</span>
                            <span class="n">exchange</span><span class="p">.</span><span class="na">getResponse</span><span class="p">().</span><span class="na">getHeaders</span><span class="p">().</span><span class="na">add</span><span class="p">(</span><span class="n">header</span><span class="p">.</span><span class="na">getKey</span><span class="p">(),</span>
                                    <span class="n">header</span><span class="p">.</span><span class="na">getValue</span><span class="p">());</span>
                        <span class="p">}</span>

                        <span class="c1">// 如果没有达到限制，允许通过，触发下一个filter</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="na">isAllowed</span><span class="p">())</span> <span class="p">{</span>
                            <span class="k">return</span> <span class="n">chain</span><span class="p">.</span><span class="na">filter</span><span class="p">(</span><span class="n">exchange</span><span class="p">);</span>
                        <span class="p">}</span>

                        <span class="c1">// 达到了限制，返回 HTTP ERROR 429</span>
                        <span class="n">setResponseStatus</span><span class="p">(</span><span class="n">exchange</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="na">getStatusCode</span><span class="p">());</span>
                        <span class="k">return</span> <span class="n">exchange</span><span class="p">.</span><span class="na">getResponse</span><span class="p">().</span><span class="na">setComplete</span><span class="p">();</span>
                    <span class="p">});</span>
                <span class="p">});</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre>
     </div>
     <h3 id="_4">
      其他的思考
     </h3>
     <p>
      个人感觉限流可以直接放到filter链的最前面，通过调整主函数的filter次序，限流filter确实可以前置，但还是要位于 GatewayMetricsFilter 后面
     </p>
     <p>
      感觉filter链可以分为三类：
     </p>
     <ul>
      <li>
       前置处理：
      </li>
      <li>
       RemoveCachedBodyFilter
      </li>
      <li>
       AdaptCachedBodyGlobalFilter
      </li>
      <li>
       NettyWriteResponseFilter : 用户返回响应给客户端
      </li>
      <li>
       ForwardPathFilter : 感觉是路径替换，有点域名查找替换的感觉
      </li>
      <li>
       <p>
        GatewayMetricsFilter ：
       </p>
      </li>
      <li>
       <p>
        特定路由处理：针对特定路由的filter，如AddRequestParameter、AddResponseHeader等待
       </p>
      </li>
      <li>
       <p>
        后置处理：
       </p>
      </li>
      <li>
       RouteToRequestUrlFilter ：好像是改path的，但具体啥作用不太清楚
      </li>
      <li>
       LoadBalancerClientFilter : 负载均衡，但已经标为将来废弃
      </li>
      <li>
       WebsocketRoutingFilter : Websocket请求的
      </li>
      <li>
       NettyRoutingFilter ：HTTP请求
      </li>
      <li>
       ForwardRoutingFilter ：
      </li>
     </ul>
     <p>
      不知道有没有全局的限流的需求，如果有的话是不是可以通过实现 global filter 进行全局限流
     </p>
     <h3 id="filter">
      Filter的小总结
     </h3>
     <p>
      在理解了request和response的处理流程以后，filter一套流程下来感觉很轻爽。它的这个思路感觉在 rpc 框架上也可以使用，进行 tag路由、负载均衡、黑名单、限流等等
     </p>
     <h2 id="_5">
      参考链接
     </h2>
     <ul>
      <li>
       <a href="https://blog.csdn.net/weixin_38405253/article/details/108891772">
        这可能是全网Spring Cloud Gateway限流最完整的方案了！
       </a>
      </li>
     </ul>
    </article>
   </div>
   <!-- Footer -->
   <footer id="footer">
    <p class="copyright">
     © Untitled. Design:
     <a href="https://html5up.net">
      HTML5 UP
     </a>
     .
    </p>
   </footer>
  </div>
  <!-- BG -->
  <div id="bg">
  </div>
  <!-- Scripts -->
  <script src="../assets/js/jquery.min.js">
  </script>
  <script src="../assets/js/browser.min.js">
  </script>
  <script src="../assets/js/breakpoints.min.js">
  </script>
  <script src="../assets/js/util.js">
  </script>
  <script src="../assets/js/main.js">
  </script>
 </body>
</html>
